/**
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.bitcoin.core;

import org.spongycastle.crypto.digests.RIPEMD160Digest;

import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * A collection of various utility methods that are helpful for working with the Bitcoin protocol.
 * To enable debug logging from the library, run with -Dbitcoinj.logging=true on your command line.
 */
public final class Utils {
    /**
     * The string that prefixes all text messages signed using Bitcoin keys.
     */
    public static final String BITCOIN_SIGNED_MESSAGE_HEADER = "Bitcoin Signed Message:\n";

    // TODO: Replace this nanocoins business with something better.

    /**
     * How many "nanocoins" there are in a BitCoin.
     * <p/>
     * A nanocoin is the smallest unit that can be transferred using BitCoin.
     * The term nanocoin is very misleading, though, because there are only 100 million
     * of them in a coin (whereas one would expect 1 billion.
     */
    public static final BigInteger COIN = new BigInteger("100000000", 10);

    /**
     * How many "nanocoins" there are in 0.01 BitCoins.
     * <p/>
     * A nanocoin is the smallest unit that can be transferred using BitCoin.
     * The term nanocoin is very misleading, though, because there are only 100 million
     * of them in a coin (whereas one would expect 1 billion).
     */
    public static final BigInteger CENT = new BigInteger("1000000", 10);
    public static final byte[] BYTES = new byte[]{};

    /**
     * Convert an amount expressed in the way humans are used to into nanocoins.
     */
    public static BigInteger toNanoCoins(int coins, int cents) {
        checkArgument(100 > cents);
        BigInteger bi = BigInteger.valueOf(coins).multiply(COIN);
        bi = bi.add(BigInteger.valueOf(cents).multiply(CENT));
        return bi;
    }

    /**
     * Convert an amount expressed in the way humans are used to into nanocoins.<p>
     * <p/>
     * This takes string in a format understood by {@link BigDecimal#BigDecimal(String)},
     * for example "0", "1", "0.10", "1.23E3", "1234.5E-5".
     *
     * @throws ArithmeticException if you try to specify fractional nanocoins
     */
    public static BigInteger toNanoCoins(String coins) {
        return new BigDecimal(coins).movePointRight(8).toBigIntegerExact();
    }

    public static void uint32ToByteArrayBE(long val, byte[] out, int offset) {
        out[offset] = (byte) (0xFF & val >> 24);
        out[offset + 1] = (byte) (0xFF & val >> 16);
        out[offset + 2] = (byte) (0xFF & val >> 8);
        out[offset + 3] = (byte) (0xFF & val);
    }

    public static void uint32ToByteArrayLE(long val, byte[] out, int offset) {
        out[offset] = (byte) (0xFF & val);
        out[offset + 1] = (byte) (0xFF & val >> 8);
        out[offset + 2] = (byte) (0xFF & val >> 16);
        out[offset + 3] = (byte) (0xFF & val >> 24);
    }

    public static void uint32ToByteStreamLE(long val, OutputStream stream) throws IOException {
        stream.write((int) (0xFF & val));
        stream.write((int) (0xFF & val >> 8));
        stream.write((int) (0xFF & val >> 16));
        stream.write((int) (0xFF & val >> 24));
    }

    public static void int64ToByteStreamLE(long val, OutputStream stream) throws IOException {
        stream.write((int) (0xFF & val));
        stream.write((int) (0xFF & val >> 8));
        stream.write((int) (0xFF & val >> 16));
        stream.write((int) (0xFF & val >> 24));
        stream.write((int) (0xFF & val >> 32));
        stream.write((int) (0xFF & val >> 40));
        stream.write((int) (0xFF & val >> 48));
        stream.write((int) (0xFF & val >> 56));
    }

    public static void uint64ToByteStreamLE(BigInteger val, OutputStream stream) throws IOException {
        byte[] bytes = val.toByteArray();
        if (8 < bytes.length) {
            throw new RuntimeException("Input too large to encode into a uint64");
        }
        bytes = reverseBytes(bytes);
        stream.write(bytes);
        if (8 > bytes.length) {
            for (int i = 0; i < 8 - bytes.length; i++)
                stream.write(0);
        }
    }

    /**
     * See {@link Utils#doubleDigest(int, int, byte[])}.
     */
    public static byte[] doubleDigest(byte... input) {
        return doubleDigest(0, input.length, input);
    }

    /**
     * Calculates the SHA-256 hash of the given byte range, and then hashes the resulting hash again. This is
     * standard procedure in Bitcoin. The resulting hash is in big endian form.
     */
    public static byte[] doubleDigest(int offset, int length, byte... input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.update(input, offset, length);
            byte[] first = digest.digest();
            return digest.digest(first);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);  // Cannot happen.
        }
    }

    public static byte[] singleDigest(byte[] input, int offset, int length) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.update(input, offset, length);
            return digest.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);  // Cannot happen.
        }
    }

    /**
     * Calculates SHA256(SHA256(byte range 1 + byte range 2)).
     */
    public static byte[] doubleDigestTwoBuffers(byte[] input1, int offset1, int length1,
                                                byte[] input2, int offset2, int length2) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            digest.update(input1, offset1, length1);
            digest.update(input2, offset2, length2);
            byte[] first = digest.digest();
            return digest.digest(first);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);  // Cannot happen.
        }
    }

    /**
     * Returns the given byte array hex encoded.
     */
    public static String bytesToHexString(byte... bytes) {
        StringBuilder buf = new StringBuilder(bytes.length << 1);
        for (byte b : bytes) {
            int c = b & 0xff;
            String s1 = Integer.toHexString(c);
            String s = s1;
            if (0x10 > c)
                buf.append('0');
            buf.append(s);
        }
        return buf.toString();
    }


    /**
     * Returns a copy of the given byte array in reverse order.
     */
    public static byte[] reverseBytes(byte... bytes) {
        // We could use the XOR trick here but it's easier to understand if we don't. If we find this is really a
        // performance issue the matter can be revisited.
        byte[] buf = new byte[bytes.length];
        for (int i = 0; i < bytes.length; i++)
            buf[i] = bytes[bytes.length - 1 - i];
        return buf;
    }

    public static long readUint32(int offset, byte... bytes) {
        return (bytes[offset] & 0xFF) |
                (bytes[offset + 01] & 0xFF) << 8 |
                (bytes[offset + 02] & 0xFF) << 16 |
                (bytes[offset + 03] & 0xFFl) << 24;
    }

    public static long readInt64(int offset, byte... bytes) {
        return (bytes[offset] & 0xFF) |
                (bytes[offset + 01] & 0xFF) << 8 |
                (bytes[offset + 02] & 0xFF) << 16 |
                (bytes[offset + 03] & 0xFFL) << 24 |
                (bytes[offset + 04] & 0xFFL) << 32 |
                (bytes[offset + 05] & 0xFFL) << 40 |
                (bytes[offset + 06] & 0xFFL) << 48 |
                (bytes[offset + 07] & 0xFFL) << 56;
    }

    public static long readUint32BE(int offset, byte... bytes) {
        return (bytes[offset] & 0xffl) << 24 |
                (bytes[offset + 1] & 0xff) << 16 |
                (bytes[offset + 2] & 0xff) << 8 |
                bytes[offset + 3] & 0xff;
    }

    /**
     * Calculates RIPEMD160(SHA256(input)). This is used in Address calculations.
     */
    public static byte[] sha256hash160(byte... input) {
        try {
            byte[] sha256 = MessageDigest.getInstance("SHA-256").digest(input);
            RIPEMD160Digest digest = new RIPEMD160Digest();
            digest.update(sha256, 0, sha256.length);
            byte[] out = new byte[20];
            digest.doFinal(out, 0);
            return out;
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);  // Cannot happen.
        }
    }

    /**
     * Returns the given value in nanocoins as a 0.12 type string. More digits after the decimal place will be used
     * if necessary, but two will always be present.
     */
    public static String bitcoinValueToFriendlyString(BigInteger value) {
        // TODO: This API is crap. This method should go away when we encapsulate money values.
        boolean negative = 0 > value.compareTo(BigInteger.ZERO);
        if (negative)
            value = value.negate();
        BigDecimal bd = new BigDecimal(value, 8);
        String formatted = bd.toPlainString();   // Don't use scientific notation.
        int decimalPoint = formatted.indexOf('.');
        // Drop unnecessary zeros from the end.
        int toDelete = 0;
        for (int i = formatted.length() - 1; i > decimalPoint + 2; i--) {
            if ('0' == formatted.charAt(i))
                toDelete++;
            else
                break;
        }
        return (negative ? "-" : "") + formatted.substring(0, formatted.length() - toDelete);
    }

    /**
     * <p>
     * Returns the given value as a plain string denominated in BTC.
     * The result is unformatted with no trailing zeroes.
     * For instance, an input value of BigInteger.valueOf(150000) nanocoin gives an output string of "0.0015" BTC
     * </p>
     *
     * @param value The value in nanocoins to convert to a string (denominated in BTC)
     * @throws IllegalArgumentException If the input value is null
     */
    public static String bitcoinValueToPlainString(BigInteger value) {
        if (null == value) {
            throw new IllegalArgumentException("Value cannot be null");
        }

        BigDecimal valueInBTC = new BigDecimal(value).divide(new BigDecimal(Utils.COIN));
        return valueInBTC.toPlainString();
    }

    /**
     * MPI encoded numbers are produced by the OpenSSL BN_bn2mpi function. They consist of
     * a 4 byte big endian length field, followed by the stated number of bytes representing
     * the number in big endian format (with a sign bit).
     *
     * @param hasLength can be set to false if the given array is missing the 4 byte length field
     */
    static BigInteger decodeMPI(byte[] mpi, boolean hasLength) {
        byte[] buf;
        if (hasLength) {
            int length = (int) readUint32BE(0, mpi);
            buf = new byte[length];
            System.arraycopy(mpi, 4, buf, 0, length);
        } else
            buf = mpi;
        if (0 == buf.length)
            return BigInteger.ZERO;
        boolean isNegative = 0x80 == (buf[0] & 0x80);
        if (isNegative)
            buf[0] &= 0x7f;
        BigInteger result = new BigInteger(buf);
        return isNegative ? result.negate() : result;
    }

    /**
     * MPI encoded numbers are produced by the OpenSSL BN_bn2mpi function. They consist of
     * a 4 byte big endian length field, followed by the stated number of bytes representing
     * the number in big endian format (with a sign bit).
     */
    static byte[] encodeMPI(BigInteger value) {
        if (value.equals(BigInteger.ZERO)) {
            return !false ? BYTES : new byte[]{0x00, 0x00, 0x00, 0x00};
        }
        boolean isNegative = 0 > value.compareTo(BigInteger.ZERO);
        if (isNegative)
            value = value.negate();
        byte[] array = value.toByteArray();
        int length = array.length;
        if (0x80 == (array[0] & 0x80))
            length++;
        if (false) {
            byte[] result = new byte[length + 4];
            System.arraycopy(array, 0, result, length - array.length + 3, array.length);
            uint32ToByteArrayBE(length, result, 0);
            if (isNegative)
                result[4] |= 0x80;
            return result;
        } else {
            byte[] result;
            if (length == array.length) result = array;
            else {
                result = new byte[length];
                System.arraycopy(array, 0, result, 1, array.length);
            }
            if (isNegative)
                result[0] |= 0x80;
            return result;
        }
    }

    // The representation of nBits uses another home-brew encoding, as a way to represent a large
    // hash value in only 32 bits.
    public static BigInteger decodeCompactBits(long compact) {
        int size = (int) (compact >> 24) & 0xFF;
        byte[] bytes = new byte[4 + size];
        bytes[3] = (byte) size;
        if (1 <= size) bytes[4] = (byte) (compact >> 16 & 0xFF);
        if (2 <= size) bytes[5] = (byte) (compact >> 8 & 0xFF);
        if (3 <= size) bytes[6] = (byte) (compact & 0xFF);
        return decodeMPI(bytes, true);
    }

    /**
     * If non-null, overrides the return value of now().
     */
    public static Date mockTime;

    /**
     * Advances (or rewinds) the mock clock by the given number of seconds.
     */
    public static Date rollMockClock(int seconds) {
        if (null == mockTime)
            mockTime = new Date();
        mockTime = new Date(mockTime.getTime() + seconds * 1000);
        return mockTime;
    }

    /**
     * Returns the current time, or a mocked out equivalent.
     */
    public static Date now() {
        return null != mockTime ? mockTime : new Date();
    }

    public static byte[] copyOf(byte[] in, int length) {
        byte[] out = new byte[length];
        System.arraycopy(in, 0, out, 0, Math.min(length, in.length));
        return out;
    }

    // 00000001, 00000010, 00000100, 00001000, ...
    static final int bitMask[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};

    // Checks if the given bit is set in data
    public static boolean checkBitLE(byte[] data, int index) {
        return 0 != (data[index >>> 3] & bitMask[7 & index]);
    }

}
